#!/usr/bin/env bash
# Copyright 1999-2023 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2

source "${PORTAGE_BIN_PATH}"/helper-functions.sh || exit 1

# avoid multiple calls to `has`.  this creates things like:
#   FEATURES_foo=false
# if "foo" is not in ${FEATURES}
tf() { "$@" && echo true || echo false ; }
exp_tf() {
	local flag var=$1
	shift
	for flag in "$@" ; do
		eval ${var}_${flag}=$(tf has ${flag} ${!var})
	done
}
exp_tf FEATURES compressdebug dedupdebug installsources nostrip splitdebug xattr
exp_tf PORTAGE_RESTRICT binchecks dedupdebug installsources splitdebug strip

if ! ___eapi_has_prefix_variables; then
	EPREFIX= ED=${D}
fi

banner=false
SKIP_STRIP=false
if ${PORTAGE_RESTRICT_strip} || ${FEATURES_nostrip} ; then
	SKIP_STRIP=true
	banner=true
	${FEATURES_installsources} || exit 0
fi

prepstrip=false

while [[ $# -gt 0 ]] ; do
	case $1 in
	--ignore)
		shift

		skip_dirs=()
		for skip; do
			if [[ -d ${ED%/}/${skip#/} ]]; then
				skip_dirs+=( "${ED%/}/${skip#/}" )
			else
				rm -f "${ED%/}/${skip#/}.estrip" || die
			fi
		done

		if [[ ${skip_dirs[@]} ]]; then
			find "${skip_dirs[@]}" -name '*.estrip' -delete || die
		fi

		exit 0
		;;
	--queue)
		shift

		find_paths=()
		for path; do
			if [[ -e ${ED%/}/${path#/} ]]; then
				find_paths+=( "${ED%/}/${path#/}" )
			fi
		done

		if (( ${#find_paths[@]} )); then
			# We can avoid scanelf calls for binaries we already
			# checked in install_qa_check (where we generate
			# NEEDED for everything installed).
			#
			# EAPI 7+ has controlled stripping (dostrip) though
			# which is why estrip has the queue/dequeue logic,
			# so we need to take the intersection of:
			# 1. files scanned earlier (all ELF installed)
			#    (note that this should be a superset of 2., so we don't
			#    need to worry about unknown files appearing)
			#
			# 2. the files we're interested in right now
			scanelf_results=()
			if [[ -f "${PORTAGE_BUILDDIR}"/build-info/NEEDED ]] ; then
				# The arguments may not be exact files (probably aren't), but search paths/directories
				# which should then be searched recursively.
				while IFS= read -r needed_entry ; do
					for find_path in "${find_paths[@]}" ; do
						# NEEDED has a bunch of entries like:
						# /usr/lib64/libfoo.so libc.so
						#
						# find_path entries may be exact paths (like /usr/lib64/libfoo.so)
						# or instead /usr/lib64, or ${ED}/usr, etc.
						#
						# We check if the beginning (i.e. first entry) of the NEEDED line
						# matches the path given
						# e.g. find_path="/usr/lib64" will match needed_entry="/usr/lib64/libfoo.so libc.so".
						needed_entry_file="${needed_entry% *}"
						if [[ "${needed_entry_file}" =~ ^${find_path##${D}} ]] ; then
							scanelf_results+=( "${D}${needed_entry_file}" )
						fi
					done
				done < "${PORTAGE_BUILDDIR}"/build-info/NEEDED
			else
				mapfile -t scanelf_results < <(scanelf -yqRBF '#k%F' -k '.symtab' "${find_paths[@]}")
			fi

			while IFS= read -r path; do
				>> "${path}.estrip" || die
			done < <(
				(( ${#scanelf_results[@]} )) && printf "%s\n" "${scanelf_results[@]}"
				find "${find_paths[@]}" -type f ! -type l -name '*.a'
			)

			unset scanelf_results needed_entry needed_entry_file find_path
		fi

		exit 0
		;;
	--dequeue)
		[[ $# -eq 1 ]] || die "${0##*/}: $1 takes no additional arguments"
		break
		;;
	--prepallstrip)
		[[ $# -eq 1 ]] || die "${0##*/}: $1 takes no additional arguments"
		prepstrip=true
		break
		;;
	*)
		die "${0##*/}: unknown arguments '$*'"
		exit 1
		;;
	esac
	shift
done
set -- "${ED}"

PRESERVE_XATTR=false
if [[ ${KERNEL} == linux ]] && ${FEATURES_xattr} ; then
	PRESERVE_XATTR=true
	if type -P getfattr >/dev/null && type -P setfattr >/dev/null ; then
		dump_xattrs() {
			getfattr -d -m - --absolute-names "$1"
		}
		restore_xattrs() {
			setfattr --restore=-
		}
	else
		dump_xattrs() {
			PYTHONPATH=${PORTAGE_PYTHONPATH:-${PORTAGE_PYM_PATH}} \
			"${PORTAGE_PYTHON:-/usr/bin/python}" \
			"${PORTAGE_BIN_PATH}/xattr-helper.py" --dump < <(echo -n "$1")
		}

		restore_xattrs() {
			PYTHONPATH=${PORTAGE_PYTHONPATH:-${PORTAGE_PYM_PATH}} \
			"${PORTAGE_PYTHON:-/usr/bin/python}" \
			"${PORTAGE_BIN_PATH}/xattr-helper.py" --restore
		}
	fi
fi

# Look up the tools we might be using
for t in STRIP:strip OBJCOPY:objcopy READELF:readelf RANLIB:ranlib ; do
	v=${t%:*} # STRIP
	t=${t#*:} # strip
	eval ${v}=\"${!v:-${CHOST}-${t}}\"
	type -P -- ${!v} >/dev/null || eval ${v}=${t}
done

# Figure out what tool set we're using to strip stuff
unset SAFE_STRIP_FLAGS DEF_STRIP_FLAGS SPLIT_STRIP_FLAGS
case $(${STRIP} --version 2>/dev/null) in
*elfutils*) # dev-libs/elfutils
	# elfutils default behavior is always safe, so don't need to specify
	# any flags at all
	SAFE_STRIP_FLAGS=""
	DEF_STRIP_FLAGS="--remove-comment"
	SPLIT_STRIP_FLAGS="-f"
	;;
*GNU*) # sys-devel/binutils
	# We'll leave out -R .note for now until we can check out the relevance
	# of the section when it has the ALLOC flag set on it ...
	SAFE_STRIP_FLAGS="--strip-unneeded -N __gentoo_check_ldflags__"
	DEF_STRIP_FLAGS="-R .comment -R .GCC.command.line -R .note.gnu.gold-version"
	SPLIT_STRIP_FLAGS=
	;;
esac
: ${PORTAGE_STRIP_FLAGS=${SAFE_STRIP_FLAGS} ${DEF_STRIP_FLAGS}}

prepstrip_sources_dir=${EPREFIX}/usr/src/debug/${CATEGORY}/${PF}

debugedit=$(type -P debugedit)
if [[ -z ${debugedit} ]]; then
	debugedit_paths=(
		"${EPREFIX}/usr/libexec/rpm/debugedit"
	)

	for x in "${debugedit_paths[@]}"; do
		if [[ -x ${x} ]]; then
			debugedit=${x}
			break
		fi
	done
fi
[[ ${debugedit} ]] && debugedit_found=true || debugedit_found=false
debugedit_warned=false

dwz=$(type -P dwz)
[[ ${dwz} ]] && dwz_found=true || dwz_found=false
dwz_warned=false

__multijob_init

# Setup ${T} filesystem layout that we care about.
tmpdir="${T}/prepstrip"
rm -rf "${tmpdir}"
mkdir -p "${tmpdir}"/{inodes,splitdebug,sources}

# Usage: save_elf_sources <elf>
save_elf_sources() {
	${FEATURES_installsources} || return 0
	${PORTAGE_RESTRICT_installsources} && return 0

	if ! ${debugedit_found} ; then
		if ! ${debugedit_warned} ; then
			debugedit_warned=true
			ewarn "FEATURES=installsources is enabled but the debugedit binary could not be"
			ewarn "found. This feature will not work unless debugedit is installed!"
		fi
		return 0
	fi

	local x=$1

	# since we're editing the ELF here, we should recompute the build-id
	# (the -i flag below).  save that output so we don't need to recompute
	# it later on in the save_elf_debug step.
	buildid=$("${debugedit}" -i \
		-b "${WORKDIR}" \
		-d "${prepstrip_sources_dir}" \
		-l "${tmpdir}/sources/${x##*/}.${BASHPID:-$(__bashpid)}" \
		"${x}")
}

# Try to create a symlink.
# Return success if it already exists.
__try_symlink() {
	local target=$1
	local name=$2

	# Check for an existing link before and after in case we are racing against
	# another process.
	[[ -L ${name} ]] ||
		ln -s "${target}" "${name}" ||
		[[ -L ${name} ]] ||
		die "failed to create symlink '${name}'"
}

# Usage: dedup_elf_debug <src> <inode_dedupdebug>
dedup_elf_debug() {
	${FEATURES_dedupdebug} || return 0
	${PORTAGE_RESTRICT_dedupdebug} && return 0

	debug-print-function "${FUNCNAME}" "$@"

	if ! ${dwz_found} ; then
		if ! ${dwz_warned} ; then
			dwz_warned=true
			ewarn "FEATURES=dedupdebug is enabled but the dwz binary could not be"
			ewarn "found. This feature will not work unless dwz is installed!"
		fi
		return 0
	fi

	local src=$1              # File to dedup debug symbols
	local inode_dedupdebug=$2 # Temp path for hard link tracking

	# We already dedupdebug-ed this inode.
	[[ -L ${inode_dedupdebug} ]] && return 0

	"${dwz}" -- "${src}"
	touch "${inode_dedupdebug}"
}

# Usage: save_elf_debug <src> <inode_debug> [splitdebug]
save_elf_debug() {
	${FEATURES_splitdebug} || return 0
	${PORTAGE_RESTRICT_splitdebug} && return 0

	debug-print-function "${FUNCNAME}" "$@"

	# NOTE: Debug files must be installed in
	# ${EPREFIX}/usr/lib/debug/${EPREFIX} (note that ${EPREFIX} occurs
	# twice in this path) in order for gdb's debug-file-directory
	# lookup to work correctly.
	local src=$1         # File from which we extract symbols.
	local inode_debug=$2 # Temp path for hard link tracking
	local splitdebug=$3  # Existing debug file optionally created by eu-strip in parent function

	# Source paths
	local src_basename=${src##*/}
	local src_dirname=${src%/*}

	# Destination paths
	local dst_dirname=${ED%/}/usr/lib/debug/${src_dirname#${D%/}/}
	local dst_basename dst

	# dont save debug info twice
	[[ ${src} == *".debug" ]] && return 0

	mkdir -p "${dst_dirname}" || die "failed to create directory '${dst_dirname}'"

	if [[ -L ${inode_debug} ]] ; then
		# We already created a debug file for this inode.
		# Read back the file name, and create another hard link if necessary.
		dst_basename=$(readlink "${inode_debug}") || die "failed to read link '${inode_debug}'"
		dst_basename=${dst_basename##*/}
		dst=${dst_dirname}/${dst_basename}
		if [[ ! -e ${dst} ]]; then
			debug-print "creating hard link: target: '${inode_debug}' name: '${dst}'"
			ln -L "${inode_debug}" "${dst}" || die "failed to create hard link '${dst}'"
		fi
	else
		dst_basename=${src_basename}.debug
		dst=${dst_dirname}/${dst_basename}
		if [[ -n ${splitdebug} ]] ; then
			mv "${splitdebug}" "${dst}"
		else
			local objcopy_flags="--only-keep-debug"
			${FEATURES_compressdebug} && objcopy_flags+=" --compress-debug-sections"
			${OBJCOPY} ${objcopy_flags} "${src}" "${dst}" &&
			${OBJCOPY} --add-gnu-debuglink="${dst}" "${src}"
		fi

		# Only do the following if the debug file was
		# successfully created (see bug #446774).
		if [[ $? -eq 0 ]] ; then
			local args="a-x,o-w"
			[[ -g ${src} || -u ${src} ]] && args+=",go-r"
			chmod ${args} "${dst}"

			# Symlink so we can read the name back.
			__try_symlink "${dst}" "${inode_debug}"

			# If we don't already have build-id from debugedit, look it up
			if [[ -z ${buildid} ]] ; then
				# convert the readelf output to something useful
				buildid=$(${READELF} -n "${src}" 2>/dev/null | awk '/Build ID:/{ print $NF; exit }')
			fi

			if [[ -n ${buildid} ]] ; then
				local buildid_dir="${ED%/}/usr/lib/debug/.build-id/${buildid:0:2}"
				local buildid_file="${buildid_dir}/${buildid:2}"
				local src_buildid_rel="../../../../../${src#${ED%/}/}"
				local dst_buildid_rel="../../${dst#${ED%/}/usr/lib/debug/}"
				mkdir -p "${buildid_dir}" || die
				__try_symlink "${dst_buildid_rel}" "${buildid_file}.debug"
				__try_symlink "${src_buildid_rel}" "${buildid_file}"
			fi
		fi
	fi

}

# Usage: process_elf <elf>
process_elf() {
	local x=$1 inode_link=$2 strip_flags=${*:3}
	local ed_noslash=${ED%/}
	local already_stripped xt_data
	local lockfile=${inode_link}_lockfile
	local locktries=100

	__vecho "   ${x:${#ed_noslash}}"

	# If two processes try to debugedit or strip the same hardlink at the
	# same time, it may corrupt files or cause loss of splitdebug info.
	# So, use a lockfile to prevent interference (easily observed with
	# dev-vcs/git which creates ~111 hardlinks to one file in
	# /usr/libexec/git-core).
	while ! ln "${inode_link}" "${lockfile}" 2>/dev/null; do
		(( --locktries > 0 )) || die "failed to acquire lock '${lockfile}'"
		sleep 1
	done

	[ -f "${inode_link}_stripped" ] && already_stripped=true || already_stripped=false

	if ! ${already_stripped} ; then
		if ${PRESERVE_XATTR} ; then
			xt_data=$(dump_xattrs "${x}")
		fi
		save_elf_sources "${x}"
		dedup_elf_debug "${x}" "${inode_link}_dedupdebug"
	fi

	if ${strip_this} ; then
		# See if we can split & strip at the same time
		if [[ -n ${SPLIT_STRIP_FLAGS} ]] ; then
			local shortname="${x##*/}.debug"
			local splitdebug="${tmpdir}/splitdebug/${shortname}.${BASHPID:-$(__bashpid)}"

			${already_stripped} || \
			${STRIP} ${strip_flags} \
				-f "${splitdebug}" \
				-F "${shortname}" \
				"${x}"
			save_elf_debug "${x}" "${inode_link}_debug" "${splitdebug}"
		else
			save_elf_debug "${x}" "${inode_link}_debug"
			${already_stripped} || ${STRIP} ${strip_flags} "${x}"
		fi
	fi

	if ${already_stripped} ; then
		rm -f "${x}" || die "rm failed unexpectedly"
		ln "${inode_link}_stripped" "${x}" || die "ln failed unexpectedly"
	else
		ln "${x}" "${inode_link}_stripped" || die "ln failed unexpectedly"
		if [[ ${xt_data} ]] ; then
			restore_xattrs <<< "${xt_data}"
		fi
	fi

	[[ -n ${lockfile} ]] && rm -f "${lockfile}"
}

# Usage: process_ar <ar archive>
process_ar() {
	local x=$1
	local ed_noslash=${ED%/}

	__vecho "   ${x:${#ed_noslash}}"

	if ${strip_this} ; then
		# If we have split debug enabled, then do not strip this.
		# There is no concept of splitdebug for objects not yet
		# linked in (only for finally linked ELFs), so we have to
		# retain the debug info in the archive itself.
		if ! ${FEATURES_splitdebug} || ${PORTAGE_RESTRICT_splitdebug} ; then
			${STRIP} -g "${x}" && ${RANLIB} "${x}"
		fi
	fi
}

# The existance of the section .symtab tells us that a binary is stripped.
# We want to log already stripped binaries, as this may be a QA violation.
# They prevent us from getting the splitdebug data.
if ! ${PORTAGE_RESTRICT_binchecks} ; then
	# We need to do the non-stripped scan serially first before we turn around
	# and start stripping the files ourselves.  The log parsing can be done in
	# parallel though.
	log=${tmpdir}/scanelf-already-stripped.log
	scanelf -yqRBF '#k%F' -k '!.symtab' "$@" | sed -e "s#^${ED%/}/##" > "${log}"

	(
	__multijob_child_init
	qa_var="QA_PRESTRIPPED_${ARCH/-/_}"
	[[ -n ${!qa_var} ]] && QA_PRESTRIPPED="${!qa_var}"
	if [[ -n ${QA_PRESTRIPPED} && -s ${log} && \
		${QA_STRICT_PRESTRIPPED-unset} = unset ]] ; then
		shopts=$-
		set -o noglob
		for x in ${QA_PRESTRIPPED} ; do
			sed -e "s#^${x#/}\$##" -i "${log}"
		done
		set +o noglob
		set -${shopts}
	fi
	sed -e "/^\$/d" -e "s#^#/#" -i "${log}"
	if [[ -s ${log} ]] ; then
		__vecho -e "\n"
		eqawarn "QA Notice: Pre-stripped files found:"
		eqawarn "$(<"${log}")"
	else
		rm -f "${log}"
	fi
	) &
	__multijob_post_fork
fi

# Since strip creates a new inode, we need to know the initial set of
# inodes in advance, so that we can avoid interference due to trying
# to strip the same (hardlinked) file multiple times in parallel.
# See bug #421099.
if  [[ ${USERLAND} == BSD ]] ; then
	get_inode_number() { stat -f '%i' "$1"; }
else
	get_inode_number() { stat -c '%i' "$1"; }
fi

cd "${tmpdir}/inodes" || die "cd failed unexpectedly"

if ${prepstrip}; then
while read -r x ; do
	inode_link=$(get_inode_number "${x}") || die "stat failed unexpectedly"
	echo "${x}" >> "${inode_link}" || die "echo failed unexpectedly"
done < <(
	# NEEDED may not exist for some packages (bug #862606)
	if [[ -f "${PORTAGE_BUILDDIR}"/build-info/NEEDED ]] ; then
		while IFS= read -r needed_entry ; do
			needed_entry="${needed_entry% *}"
			needed_contents+=( "${D%/}${needed_entry}" )
		done < "${PORTAGE_BUILDDIR}"/build-info/NEEDED
	fi

	# Use sort -u to eliminate duplicates (bug #445336).
	(
		[[ -n ${needed_contents[@]} ]] && printf "%s\n" "${needed_contents[@]}"
		find "$@" -type f ! -type l -name '*.a'
	) | LC_ALL=C sort -u
)
else
while IFS= read -d '' -r x ; do
	inode_link=$(get_inode_number "${x%.estrip}") || die "stat failed unexpectedly"
	echo "${x%.estrip}" >> "${inode_link}" || die "echo failed unexpectedly"
done < <(find "${ED}" -name '*.estrip' -delete -print0)
fi

# Now we look for unstripped binaries.
for inode_link in $(shopt -s nullglob; echo *) ; do
while read -r x
do

	if ! ${banner} ; then
		__vecho "strip: ${STRIP} ${PORTAGE_STRIP_FLAGS}"
		banner=true
	fi

	(
	__multijob_child_init
	f=$(file -S "${x}") || exit 0
	[[ -z ${f} ]] && exit 0

	if ${SKIP_STRIP} ; then
		strip_this=false
	elif ! ${prepstrip}; then
		strip_this=true
	else
		# The noglob funk is to support STRIP_MASK="/*/booga" and to keep
		# the for loop from expanding the globs.
		# The eval echo is to support STRIP_MASK="/*/{booga,bar}".
		set -o noglob
		strip_this=true
		for m in $(eval echo ${STRIP_MASK}) ; do
			[[ ${x#${ED%/}} == ${m} ]] && strip_this=false && break
		done
		set +o noglob
	fi

	# In Prefix we are usually an unprivileged user, so we can't strip
	# unwritable objects.  Make them temporarily writable for the
	# stripping.
	was_not_writable=false
	if [[ ! -w ${x} ]] ; then
		was_not_writable=true
		chmod u+w "${x}"
	fi

	# only split debug info for final linked objects
	# or kernel modules as debuginfo for intermediatary
	# files (think crt*.o from gcc/glibc) is useless and
	# actually causes problems.  install sources for all
	# elf types though because that stuff is good.

	buildid=
	if [[ ${f} == *"current ar archive"* ]] ; then
		process_ar "${x}"
	elif [[ ${f} == *"SB executable"* || ${f} == *"SB pie executable"* ||
		${f} == *"SB shared object"* ]] ; then
		process_elf "${x}" "${inode_link}" ${PORTAGE_STRIP_FLAGS}
	elif [[ ${f} == *"SB relocatable"* ]] ; then
		process_elf "${x}" "${inode_link}" ${SAFE_STRIP_FLAGS}
	fi

	if ${was_not_writable} ; then
		chmod u-w "${x}"
	fi
	) &
	__multijob_post_fork

done < "${inode_link}"
done

# With a bit more work, we could run the rsync processes below in
# parallel, but not sure that'd be an overall improvement.
__multijob_finish

cd "${tmpdir}"/sources/ && cat * > "${tmpdir}/debug.sources" 2>/dev/null
if [[ -s ${tmpdir}/debug.sources ]] && \
   ${FEATURES_installsources} && \
   ! ${PORTAGE_RESTRICT_installsources} && \
   ${debugedit_found}
then
	__vecho "installsources: rsyncing source files"
	[[ -d ${D%/}/${prepstrip_sources_dir#/} ]] || mkdir -p "${D%/}/${prepstrip_sources_dir#/}"

	# Skip installation of ".../<foo>" (system headers? why inner slashes are forbidden?)
	# Skip syncing of ".../foo/" (complete directories)
	grep -zv -e '/<[^/>]*>$' -e '/$' "${tmpdir}"/debug.sources | \
		(cd "${WORKDIR}"; LANG=C sort -z -u | \
		rsync -tL0 --chmod=ugo-st,a+r,go-w,Da+x,Fa-x --files-from=- "${WORKDIR}/" "${D%/}/${prepstrip_sources_dir#/}/" )

	# Preserve directory structure.
	# Needed after running save_elf_sources.
	# https://bugzilla.redhat.com/show_bug.cgi?id=444310
	while read -r -d $'\0' emptydir; do
		>> "${emptydir}"/.keepdir
	done < <(find "${D%/}/${prepstrip_sources_dir#/}/" -type d -empty -print0)
fi

cd "${T}"
rm -rf "${tmpdir}"
